﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="InvokeMethodBinder.cs" company="JDML Consulting Ltd.">
//   Copyright 2011 Photon Project - http://photon.codeplex.com
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace Photon.Windows.Input
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using Collections;

    public class InvokeMethodBinder
    {
        #region Fields

        private readonly Dictionary<ExpressionKey, InvokeMethodBinding> _cache = new Dictionary<ExpressionKey, InvokeMethodBinding>();
        private readonly InvokeMethodExpressionParser _parser = new InvokeMethodExpressionParser();

        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="InvokeMethodBinder"/> class.
        /// </summary>
        public InvokeMethodBinder() : this(500)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="InvokeMethodBinder"/> class.
        /// </summary>
        public InvokeMethodBinder(int maxCacheSize)
        {
            MaxCacheSize = maxCacheSize;
        }

        /// <summary>
        /// Binds the specified target type.
        /// </summary>
        /// <param name="targetType">Type of the target.</param>
        /// <param name="expression">The expression.</param>
        /// <returns></returns>
        public InvokeMethodBinding Bind(Type targetType, string expression)
        {
            var key = new ExpressionKey(targetType, expression);

            var result = GetBindingFromCache(key);
            if (result == null)
            {
                result = GetBindingFromTarget(targetType, expression);
                if (result != null)
                {
                    AddToCache(key, result);
                }
            }

            return result;
        }

        /// <summary>
        /// Adds to cache.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="result">The result.</param>
        private void AddToCache(ExpressionKey key, InvokeMethodBinding result)
        {
            if (this._cache.Count > MaxCacheSize)
            {
                this._cache.Clear();
            }

            this._cache.Add(key, result);
        }

        /// <summary>
        /// Gets the size of the max cache.
        /// </summary>
        /// <value>The size of the max cache.</value>
        public int MaxCacheSize { get; private set; }

        private InvokeMethodBinding GetBindingFromCache(ExpressionKey key)
        {
            InvokeMethodBinding result;
            _cache.TryGetValue(key, out result);
            return result;
        }

        private InvokeMethodBinding GetBindingFromTarget(IReflect targetType, string expression)
        {
            var parsedExpression = this._parser.Parse(expression);

            var executeMethod = GetMatchingMethod(targetType, parsedExpression.MethodName, parsedExpression.ParameterTypes, null);
            if (executeMethod != null)
            {
                var canExecuteMethod = GetMatchingMethod(targetType, "Can" + parsedExpression.MethodName, 
                    parsedExpression.ParameterTypes, x => x.ReturnType.Equals(typeof(bool)));
                if (canExecuteMethod == null)
                {
                    canExecuteMethod = GetCanExecutePropertyMethod(targetType, "Can" + parsedExpression.MethodName);
                }

                return new InvokeMethodBinding(executeMethod, canExecuteMethod);
            }

            return null;
        }

        private static MethodInfo GetCanExecutePropertyMethod(IReflect targetType, string propertyName)
        {
            var property = targetType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
            if (property != null && property.CanRead && property.PropertyType.Equals(typeof(bool)))
            {
                return property.GetGetMethod();
            }

            return null;
        }

        private static MethodInfo GetMatchingMethod(IReflect targetType, string methodName, IList<InvokeMethodTypeReference> parameterTypes, 
            Predicate<MethodInfo> predicate)
        {
            var matchingMethods = GetMatchingMethods(targetType, methodName, parameterTypes, predicate);
            switch (matchingMethods.Count)
            {
                case 0:
                    return null;
                case 1:
                    return matchingMethods[0];
                default:
                    throw new AmbiguousMatchException();
            }
        }

        private static IList<MethodInfo> GetMatchingMethods(IReflect targetType, string methodName, 
            IList<InvokeMethodTypeReference> parameterTypes, 
            Predicate<MethodInfo> predicate)
        {
            var candidates = new List<MethodInfo>(targetType.GetMethods(BindingFlags.Instance | BindingFlags.Public).Where(
                x => x.Name == methodName));
            RemoveInvalidMethods(candidates, parameterTypes, predicate);
            return candidates;
        }

        private static void RemoveInvalidMethods(List<MethodInfo> methods, IList<InvokeMethodTypeReference> parameterTypes, 
            Predicate<MethodInfo> predicate)
        {
            for (var i = 0; i < methods.Count;)
            {
                var method = methods[i];

                var parameters = method.GetParameters();
                if (!IsParameterMatch(parameters.Select(x => x.ParameterType).ToList(), parameterTypes) ||
                    (predicate != null && !predicate(method)))
                {
                    methods.RemoveAt(i);
                }
                else
                {
                    i++;
                }
            }

            if (methods.Count > 1)
            {
                methods.RemoveAll(method => method.GetParameters().Length != parameterTypes.Count);
            }
        }

        private static bool IsParameterMatch(IList<Type> parameterTypes, IList<InvokeMethodTypeReference> methodParameters)
        {
            if (parameterTypes.Count < methodParameters.Count)
            {
                return false;
            }

            var minCount = Math.Min(parameterTypes.Count, methodParameters.Count);
            for (var i = 0; i < minCount; i++)
            {
                var typeName = methodParameters[i].TypeName;
                if (!string.IsNullOrEmpty(typeName))
                {
                    var parameterType = parameterTypes[i];
                    if (!GetAbreviatedTypeName(parameterType.FullName).EndsWith(typeName) &&
                        !IsAliasMatch(typeName, parameterType))
                    {
                        return false;
                    }

                    if (parameterType.IsGenericType && !IsParameterMatch(
                        parameterType.GetGenericArguments(), methodParameters[i].GenericArgumentTypes))
                    {
                        return false;
                    }

                    if (methodParameters[i].IsGenericType && !parameterType.IsGenericType)
                    {
                        return false;
                    }
                }
            }

            return true;
        }

        private static string GetAbreviatedTypeName(string typeName)
        {
            var index = typeName.IndexOf('`');
            if (index != -1)
            {
                return typeName.Substring(0, index);
            }

            return typeName;
        }

        private static bool IsAliasMatch(string typeName, Type parameterType)
        {
            if (parameterType.IsEnum)
            {
                return false;
            }

            switch (Type.GetTypeCode(parameterType))
            {
                case TypeCode.Byte:
                    return typeName == "byte";
                case TypeCode.SByte:
                    return typeName == "sbyte";
                case TypeCode.Int16:
                    return typeName == "short";
                case TypeCode.Int32:
                    return typeName == "int";
                case TypeCode.Int64:
                    return typeName == "long";
                case TypeCode.UInt16:
                    return typeName == "ushort";
                case TypeCode.UInt32:
                    return typeName == "uint";
                case TypeCode.UInt64:
                    return typeName == "ulong";
                case TypeCode.Double:
                    return typeName == "double";
                case TypeCode.Single:
                    return typeName == "float";
                case TypeCode.Decimal:
                    return typeName == "decimal";
                case TypeCode.String:
                    return typeName == "string";
                case TypeCode.Char:
                    return typeName == "char";
                case TypeCode.Boolean:
                    return typeName == "bool";
                case TypeCode.Object:
                    return parameterType.Equals(typeof(object)) && typeName == "object";
            }

            return false;
        }

        private class ExpressionKey
        {
            public ExpressionKey(Type targetType, string expression)
            {
                this.Expression = expression;
                this.TargetType = targetType;
            }

            public string Expression { get; private set; }

            public Type TargetType { get; private set; }

            public bool Equals(ExpressionKey other)
            {
                if (ReferenceEquals(null, other))
                {
                    return false;
                }

                if (ReferenceEquals(this, other))
                {
                    return true;
                }

                return Equals(other.Expression, this.Expression) && Equals(other.TargetType, this.TargetType);
            }

            public override bool Equals(object obj)
            {
                return Equals(obj as ExpressionKey);
            }

            public override int GetHashCode()
            {
                unchecked
                {
                    return (this.Expression.GetHashCode() * 397) ^ this.TargetType.GetHashCode();
                }
            }
        }
    }
}